Skip to content

C++17 新特性

目录

  1. 结构化绑定 (Structured Bindings)
  2. ifswitch 语句初始化 (Initialization in if and switch)
  3. 内联变量 (Inline Variables)
  4. constexpr Lambda 表达式
  5. 类模板参数推导 (Class Template Argument Deduction)
  6. std::variant
  7. std::optional
  8. std::any
  9. std::string_view
  10. 文件系统库 (File System Library)
  11. 其他特性
  12. 总结

1. 结构化绑定 (Structured Bindings)

C++17引入了结构化绑定,允许你将一个结构体、类、数组或元组的成员直接绑定到多个变量上。

C++17 之前:

cpp
std::pair<int, std::string> getStudent() {
    return {123, "Alice"};
}

auto student = getStudent();
int id = student.first;
std::string name = student.second;

C++17:

cpp
std::pair<int, std::string> getStudent() {
    return {123, "Alice"};
}

auto [id, name] = getStudent(); // 使用结构化绑定

示例:

cpp
#include <iostream>
#include <tuple>
#include <string>

std::tuple<int, double, std::string> getPerson() {
  return std::make_tuple(25, 1.75, "Bob");
}

int main() {
  auto [age, height, name] = getPerson();

  std::cout << "Age: " << age << std::endl;
  std::cout << "Height: " << height << std::endl;
  std::cout << "Name: " << name << std::endl;

  return 0;
}

结构化绑定可以与 const& 以及 && 结合:

c++
const auto& [constId, constName] = getStudentInfo();

2. ifswitch 语句初始化

C++17 允许在 ifswitch 语句中直接初始化变量,这可以有效地限制变量的作用域。

C++17 之前:

cpp
{ // 需要额外的作用域
    auto value = getValue();
    if (value > 10) {
        // ...
    }
}

C++17:

cpp
if (auto value = getValue(); value > 10) {
    // ...
}

switch (auto status = getStatus(); status) {
    case Success: // ...
    case Failure: // ...
}

示例:

cpp
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
  std::vector<int> numbers = {1, 2, 3, 4, 5};

  // 在 if 语句中初始化并查找元素
  if (auto it = std::find(numbers.begin(), numbers.end(), 3); it != numbers.end()) {
    std::cout << "Found at index: " << std::distance(numbers.begin(), it) << std::endl;
  } else {
    std::cout << "Not found" << std::endl;
  }
  
    // 在 switch 语句中初始化状态码
    switch (int status = 0; status)  // 这里故意使用分号来表示初始化语句
    {
        case 0:
              std::cout << "Status: OK" << std::endl;
              break;
        case 1:
              std::cout << "Status: Warning" << std::endl;
               break;
        default:
                std::cout << "Status: Error" << std::endl;
    }

  return 0;
}

注意事项: ifswitch 中的初始化语句和条件之间使用分号 ; 分隔。

3. 内联变量 (Inline Variables)

C++17 允许在头文件中定义内联变量,类似于内联函数。这对于在头文件中定义静态成员变量或全局变量非常有用。

C++17 之前:

cpp
// header.h
extern int myGlobal; // 声明

// source.cpp
int myGlobal = 42; // 定义

C++17:

cpp
// header.h
inline int myGlobal = 42; // 声明并定义

示例:

cpp
// my_class.h
#ifndef MY_CLASS_H
#define MY_CLASS_H

#include <string>

struct MyClass {
  static inline std::string version = "1.0"; // 内联静态成员变量
};

#endif
cpp
// main.cpp
#include <iostream>
#include "my_class.h"

int main() {
  std::cout << "Version: " << MyClass::version << std::endl;
  return 0;
}

4. constexpr Lambda 表达式

C++17 允许 Lambda 表达式被声明为 constexpr,这意味着它们可以在编译时执行。 即使没有显式声明constexpr, 如果Lambda表达式满足constexpr函数的要求,它也可能是constexpr的.

cpp
auto square = [](int n) constexpr {
    return n * n;
};
static_assert(square(5) == 25);

示例:

cpp
#include <iostream>
#include <array>

int main() {
  constexpr auto increment = [](int n) constexpr {
    return n + 1;
  };

  // 在编译时计算数组大小
  std::array<int, increment(5)> arr;
  std::cout << "Array size: " << arr.size() << std::endl; // 输出 6
    
   constexpr int result = increment(10); //编译时计算
   std::cout << result << std::endl;
  return 0;
}

注意:

  • constexpr lambda 的捕获必须是常量表达式。
  • 隐式 constexpr lambda: 如果一个 lambda 表达式满足 constexpr 函数的所有要求,即使没有显式地声明为 constexpr,它也可能是 constexpr 的。

5. 类模板参数推导 (Class Template Argument Deduction)

C++17 允许编译器自动推导类模板的参数,类似于函数模板的参数推导。

C++17 之前:

cpp
std::pair<int, double> p(1, 2.5);
std::vector<int> v = {1, 2, 3};

C++17:

cpp
std::pair p(1, 2.5);
std::vector v = {1, 2, 3};
std::tuple t(1, 2.5, "hello"); // 无需 <int, double, const char*>

示例:

cpp
#include <iostream>
#include <vector>
#include <utility>

int main() {
  std::pair p(1, 3.14);   // 推导为 std::pair<int, double>
  std::vector v{1, 2, 3}; // 推导为 std::vector<int>

  std::cout << "Pair: " << p.first << ", " << p.second << std::endl;
  std::cout << "Vector: ";
  for (int x : v) {
    std::cout << x << " ";
  }
  std::cout << std::endl;

  return 0;
}

可以通过提供推导指引来定制推导过程:

c++
template<typename T>
struct MyContainer {
    std::vector<T> data;
    MyContainer(T value) : data{value} {}
    MyContainer(std::initializer_list<T> list) : data(list) {}
};

// 推导指引
template<typename T>
MyContainer(T) -> MyContainer<T>;

6. std::variant

std::variant 提供了一种类型安全的联合体,可以存储多种不同类型的值,但在任何时刻只能存储其中一种类型的值。

cpp
#include <variant>

std::variant<int, double, std::string> v;
v = 10; // 存储 int
v = 3.14; // 存储 double
v = "hello"; // 存储 string

// 访问
std::get<int>(v); // 如果 v 不是 int,会抛出 std::bad_variant_access 异常
if (auto p = std::get_if<int>(&v)) { // 安全访问
  // ...
}

// 访问者模式
std::visit([](auto&& arg){ std::cout << arg << std::endl; }, v);

优点: * 类型安全,避免了传统union可能导致的类型错误。 * 可以存储多种不同类型,提高了灵活性。

示例:

c++
#include <iostream>
#include <variant>
#include <string>

int main() {
    std::variant<int, double, std::string> myVar;

    myVar = 10; // 现在存储 int
    std::cout << "Int value: " << std::get<int>(myVar) << std::endl;

    myVar = 3.14; // 现在存储 double
    std::cout << "Double value: " << std::get<double>(myVar) << std::endl;

    myVar = "Hello"; // 现在存储 string
    // 访问时需要小心处理异常,或使用 std::get_if
    if (auto p = std::get_if<std::string>(&myVar)) {
        std::cout << "String value: " << *p << std::endl;
    }
    
    //使用访问者模式
     std::visit([](auto&& arg){
        std::cout << "Variant value: " << arg << std::endl;
    }, myVar);

    return 0;
}

7. std::optional

std::optional 表示一个可能存在也可能不存在的值。它类似于指针,但更安全,因为它不会有空指针解引用的风险。

cpp
#include <optional>

std::optional<int> getValue(bool valid) {
    if (valid) {
        return 42;
    } else {
        return std::nullopt; // 表示不存在
    }
}

auto result = getValue(true);
if (result) { // 检查是否存在
    std::cout << *result << std::endl; // 安全访问
}
if (result.has_value())
{
    std::cout << result.value() << std::endl;
}
result.value_or(0); //提供默认值

示例:

c++
#include <iostream>
#include <optional>
#include <string>

std::optional<std::string> getName(bool valid) {
    if (valid) {
        return "Alice";
    } else {
        return std::nullopt;
    }
}

int main() {
    auto name1 = getName(true);
    if (name1) {
        std::cout << "Name: " << *name1 << std::endl; // 安全访问
    } else {
        std::cout << "Name not available." << std::endl;
    }

    auto name2 = getName(false);
    if (name2.has_value()) {
        std::cout << "Name: " << name2.value() << std::endl;
    } else {
        std::cout << "Name not available." << std::endl;
    }

    std::cout << "Default name: " << name2.value_or("Default") << std::endl; // 提供默认值

    return 0;
}

8. std::any

std::any 可以存储任何类型的值,类似于 void*,但类型安全。

c++
#include <any>
#include <iostream>
std::any a = 1;
a = "hello";
a = std::string("world");

//访问:
std::any_cast<int>(a); // 如果 a 不是 int, 会抛出异常
if (auto p = std::any_cast<int>(&a)) //安全的形式
{
}

示例:

c++
#include <iostream>
#include <any>
#include <string>
#include <vector>

int main() {
    std::any value;

    value = 42; // 存储 int
    std::cout << "Int value: " << std::any_cast<int>(value) << std::endl;

    value = std::string("Hello"); // 存储 string
     if (auto p = std::any_cast<std::string>(&value)) {
        std::cout << "String value: " << *p << std::endl;  //安全访问
    }
     
    value = std::vector<int>{1, 2, 3};

    return 0;
}

9. std::string_view

std::string_view 提供了一个字符串的非拥有视图,它可以指向一个 std::string、字符数组或字符串字面量,而无需复制字符串内容。

c++
#include <string_view>

void print(std::string_view sv) { // 接受 string_view
    std::cout << sv << std::endl;
}

std::string s = "hello";
print(s); // 传入 std::string
print("world"); // 传入字符串字面量

优点:

  • 避免不必要的字符串拷贝,提高性能。
  • 可以统一处理多种字符串类型。

示例:

c++
#include <iostream>
#include <string_view>
#include <string>

void printString(std::string_view str) {
    std::cout << "String: " << str << std::endl;
    std::cout << "Size: " << str.size() << std::endl;
    std::cout << "First character: " << str[0] << std::endl;
    //str[0] = 'a'; 不要尝试修改, string_view 本身并不拥有数据
}

int main() {
    std::string s = "Hello, world!";
    std::string_view sv1 = s; // 指向 std::string
    std::string_view sv2 = "This is a literal"; // 指向字符串字面量
    const char* cstr = "C-style string";
    std::string_view sv3 = cstr;

    printString(sv1);
    printString(sv2);
    printString(sv3);

    return 0;
}

注意事项:

  • std::string_view 不拥有字符串数据,因此要确保它所指向的字符串在 string_view 的生命周期内有效。
  • 不要尝试通过std::string_view 修改字符串内容, 因为他可能指向常量字符串字面量.

10. 文件系统库 (File System Library)

C++17 引入了 <filesystem> 库,提供了一组用于操作文件和目录的类和函数。

cpp
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
  fs::path p = "my_directory";
  fs::create_directory(p); // 创建目录
  fs::path file = p / "my_file.txt"; // 路径拼接
  // 写入文件
  // ...
  if (fs::exists(file)) {
      std::cout << "File size: " << fs::file_size(file) << std::endl;
      fs::remove(file); // 删除文件
  }
  fs::remove(p);
}

主要功能:

  • 路径操作 (拼接、分解、规范化等) fs::path
  • 目录操作 (创建、删除、遍历) fs::create_directory, fs::remove, fs::directory_iterator
  • 文件操作 (查询属性、复制、移动、删除) fs::exists, fs::file_size, fs::copy_file, fs::rename
  • 权限管理 fs::permissions
  • 错误处理 fs::filesystem_error

示例:

cpp
#include <iostream>
#include <fstream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
  fs::path currentDir = fs::current_path();
  std::cout << "Current directory: " << currentDir << std::endl;

  fs::path newDir = currentDir / "my_directory";
  fs::create_directory(newDir); // 创建子目录
  std::cout << "Created directory: " << newDir << std::endl;

  fs::path filePath = newDir / "my_file.txt";
  std::ofstream outFile(filePath); // 创建并写入文件
  outFile << "Hello, file system!" << std::endl;
  outFile.close();

   std::cout << "File created: " << filePath << std::endl;
   std::cout << "File size: " << fs::file_size(filePath) << " bytes" << std::endl;

  // 遍历目录
    std::cout << "Files in " << newDir << ":" << std::endl;
  for (const auto& entry : fs::directory_iterator(newDir)) {
    std::cout << entry.path() << std::endl;
  }

  // 删除文件和目录
  fs::remove(filePath);
  fs::remove(newDir);

   std::cout << "File and directory removed." << std::endl;
  return 0;
}

11. 其他特性

  • 简化的嵌套命名空间定义:

    c++
    // C++17 之前
    namespace A {
    namespace B {
    namespace C {
    // ...
    }
    }
    }
    
    // C++17
    namespace A::B::C {
    // ...
    }
  • 新的求值顺序保证: C++17 规定了某些表达式的求值顺序,以减少未定义行为的可能性。

  • __has_include 预处理表达式:用于检查头文件是否存在.

  • 保证的复制消除(Guaranteed copy elision):

c++
struct MyType {
    MyType() { std::cout << "Constructor\n"; }
    MyType(const MyType&) { std::cout << "Copy constructor\n"; }
};

MyType createMyType() {
    return MyType(); // 这里不会调用复制构造函数,直接在返回值处构造对象
}

int main()
{
    MyType obj = createMyType();
}
  • 更严格的表达式求值顺序: C++17 对表达式的求值顺序做了更严格的规定, 以减少未定义行为。

12. 总结

C++17 带来了许多令人兴奋的新特性,这些特性使 C++ 编程更加现代化、高效和安全。

  • 结构化绑定 简化了从复杂数据结构中提取数据的过程。
  • ifswitch 初始化 增强了代码的可读性和安全性。
  • 内联变量 简化了头文件中的变量定义。
  • constexpr Lambda 扩展了编译时计算的能力。
  • 类模板参数推导 减少了模板代码的冗余。
  • std::variantstd::optionalstd::any 提供了更安全、更灵活的数据存储方式。
  • std::string_view 提高了字符串处理的效率。
  • 文件系统库 提供了跨平台的文件和目录操作接口。
  • 其他特性 简化命名空间, 减少未定义行为等。

希望通过本节课的学习,大家能够掌握 C++17 的新特性,并在实际开发中灵活运用,写出更现代、更优雅、更高效的 C++ 代码。

课后作业:统计单词频率

题目描述:

给定一个文本文件(input.txt),其中包含多行英文文本。编写一个C++程序,统计文件中每个单词出现的频率,并按频率从高到低输出结果。

要求:

  1. 程序需要从文件读取文本。
  2. 忽略标点符号(如., ,, !, ?等),并将所有单词转换为小写。
  3. 使用C++17的以下特性:
    • 文件系统库 (filesystem):用于文件操作。
    • 结构化绑定 (structured bindings):用于遍历 std::map
    • if 语句初始化 (if statement with initializer):用于在循环内部更简洁地处理插入/更新操作。
    • std::string_view: 避免不必要的字符串拷贝.

输出:

程序应输出一个单词频率列表,每行包含一个单词及其出现的次数,按频率从高到低排序。例如:

the: 15
and: 10
to: 8
a: 7
of: 6
...

代码实现 (C++17):

cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <string_view>

namespace fs = std::filesystem;

// 函数:将字符串转换为小写并移除标点符号
std::string cleanWord(std::string_view word) {
    std::string result;
    for (char c : word) {
        if (std::isalpha(c)) {  // 检查是否是字母
            result += std::tolower(c);
        }
    }
    return result;
}

int main() {
    fs::path filePath = "input.txt"; // 文件路径

    // 检查文件是否存在
      if (!fs::exists(filePath))
      {
        std::cerr << "Error: File not found: " << filePath << std::endl;
         return 1;
      }

    std::ifstream file(filePath);
    if (!file.is_open()) {
        std::cerr << "Error opening file: " << filePath << std::endl;
        return 1;
    }

    std::map<std::string, int> wordCounts;
    std::string line;
    size_t totalWords = 0;  // 用于统计单词总数 (可选)
    
    while (std::getline(file, line)) {
        std::istringstream lineStream(line);
        std::string word;
        while (lineStream >> word) {
             std::string cleaned = cleanWord(word);
             if (!cleaned.empty()) {  // 确保清理后的单词非空
                  // 使用 if 语句初始化来简化插入/更新
                if (auto [it, inserted] = wordCounts.insert({cleaned, 1}); !inserted) {
                    it->second++; // 如果已存在,增加计数
                }
                totalWords++; //(可选)
             }
        }
    }
    file.close();

    // 将 map 转换为 vector 以便排序
    std::vector<std::pair<std::string, int>> sortedCounts(wordCounts.begin(), wordCounts.end());
    std::sort(sortedCounts.begin(), sortedCounts.end(),
              [](const auto& a, const auto& b) {
                  return a.second > b.second; // 按频率降序排序
              });

    // 输出结果
    std::cout << "Word frequencies (total " << totalWords << " words):" << std::endl; //(可选)
    for (const auto& [word, count] : sortedCounts) { // 使用结构化绑定
        std::cout << word << ": " << count << std::endl;
    }

    return 0;
}